package com.qs.commontools.utils.http;

import com.alibaba.fastjson.JSON;
import com.qs.commontools.common.service.HttpService;
import com.qs.commontools.constants.HttpToolConstant;
import com.qs.commontools.utils.base.StringUtils;
import com.qs.commontools.utils.collection.MapUtils;
//import sun.net.www.protocol.http.HttpURLConnection;
import java.net.HttpURLConnection;

import java.io.DataOutputStream;
import java.io.Serializable;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;

/**
 * Http入参实体
 * @author qiusuo
 * @since 2021-09-03
 */
public class HttpTool implements Serializable {

    private static final long serialVersionUID = -2656735117514836049L;

    /**
     * url路径：http类型的地址：如http://www.baidu.com/
     */

    private String url = null;

    /**
     * String类型参数：如 {@code name="zhangsan"&age=12&gender="1"}  这种
     */
    private String param = null;

    /**
     * map类型参数：Map类型
     */
    private Map<String, Object> paramMap = null;

    /**
     * 传参附件是否要求同名
     */
    private String sameName = null;

    /**
     * 附件传输唯一标识
     */
    private String BOUNDARY = null;

    /**
     * 设置请求方法
     */
    private String requestMethod = null;

    /**
     * 设置连接时间(连接主机超时)
     */
    private Integer connectTimeout = null;

    /**
     * 设置读取时间(缓存最长时间)
     */
    private Integer readTimeout = null;

    /**
     * 设置请求头
     */
    private Map<String,String> header = null;

    /**
     * 获取的是否是二进制的文本文件，默认不是false
     */
    private Boolean isTXT = null;

    /**
     * 代理地址
     */
    private String proxyUrl = null;

    /**
     * 代理端口
     */
    private Integer proxyPort = null;

    /**
     * 请求结果
     */
    private String result = null;

    /**
     * 请求状态码
     */
    private Integer code = null;

    /**
     * 请求信息
     */
    private String msg = null;


    private HttpTool(){
        this.header = new HashMap<>(HttpToolConstant.NUM_INTEGER_SIXTEEN);
    }

    /**
     * 获得实体方法
     * @return http工具类实例
     */
    public static HttpTool getInstance(){
        return init(new HttpTool());
    }

    /**
     * 获得实体方法
     * @param url 设置请求url
     * @return http工具类实例
     */
    public static HttpTool getInstance(String url) {
        HttpTool tool = new HttpTool();
        tool.setUrl(url);
        init(tool);
        return tool;
    }

    /**
     * 初始化
     * @param tool 初始化对象
     * @return 初始化后对象
     */
    private static HttpTool init(HttpTool tool) {
        // 设置请求方法默认是get
        tool.setRequestMethod(HttpToolConstant.REQUEST_METHOD_GET);
        // 设置连接时间(连接主机超时),默认为10秒
        tool.setConnectTimeout(10);
        // 设置读取时间(缓存最长时间)，默认为30秒
        tool.setReadTimeout(30);
        // 设置客户端希望接受的数据类型，默认接受全部
        tool.setAcceptType(HttpToolConstant.ACCEPT_ALL);
        // 设置TCP连接属性Connection:keep-alive/close
        tool.setConnect(true);
        // 设置请求类型,默认为表单传输
        tool.setContentType(HttpToolConstant.CONTENT_TYPE_FORM_DATA);
        // 设置请求的编码，默认UTF-8
        tool.setCharset(HttpToolConstant.CODE_FORMAT_UTF_8);
        // 设置浏览器版本
        tool.setUserAgent("PostmanRuntime/7.28.4");
        // 设置附件传输唯一标识
        tool.setBOUNDARY(StringUtils.getSimpleUUID());
        // 设置获取的是否是二进制的文本文件，默认不是false
        tool.setTXT(false);
        return tool;
    }

    // --------------------------------------------------------------------------------------
    // -----------getset----------------------------------------------------------------
    // ---------------------------------------------------------------------------------------

    /**
     * 获得序列号。
     *
     * @return 序列号
     */
    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    /**
     * 获得请求结果。
     *
     * @return 请求结果
     */
    public String getResult() {
        return result;
    }

    /**
     * 设置请求结果。
     *
     * @param result 请求结果
     */
    private void setResult(String result) {
        this.result = result;
    }

    /**
     * 获得请求状态码。
     *
     * @return 请求状态码
     */
    public Integer getCode() {
        return code;
    }

    /**
     * 设置请求状态码。
     *
     * @param code 请求状态码
     */
    private void setCode(Integer code) {
        this.code = code;
    }

    /**
     * 获得请求信息。
     *
     * @return 请求信息
     */
    public String getMsg() {
        return msg;
    }

    /**
     * 设置请求信息。
     *
     * @param msg 请求信息
     */
    private void setMsg(String msg) {
        this.msg = msg;
    }

    /**
     * 获得字符串入参。
     * <br>如果入参是字符串就是获得字符串入参，如果入参是Map，返回值的就是Map入参的JSON字符串。
     *
     * @return 字符串入参
     */
    public String getParam() {
        if (StringUtils.isBlank(param)) {
            return JSON.toJSONString(paramMap);
        }
        return param;
    }

    /**
     * 设置字符串参数。
     * 如 {@code name="zhangsan"&age=12&gender="1"} 这种,或者直接传入字符串。
     * @param param 入参
     * @return HttpTool实例
     */
    public HttpTool setParam(String param) {

        this.param = param.trim();

        // 如果入参包含"&"符号代表最起码有两个及以上的键值对参数
        if (param.contains("&")) {
            // 将所有入参切割为name="zhangsan"、age=12、gender="1"
            String[] split = param.trim().split(HttpToolConstant.CHAR_AND);
            Map<String, Object> map = new HashMap<>(HttpToolConstant.NUM_INTEGER_SIXTEEN);
            for (String str: split) {
                String[] split1 = str.split(HttpToolConstant.CHAR_EQUAL);
                map.put(split1[0], split1[1]);
            }
            // 将所有name="zhangsan"&age=12&gender="1"转为Map对象进行设置
            setParam(map);
        } else {
            // 仅有一个键值对参数或仅有一个普通字符串参数
            // 大部分情况下，带有"="符号的是键值对
            if (param.contains(HttpToolConstant.CHAR_EQUAL)) {
                Map<String, Object> map = new HashMap<>(HttpToolConstant.NUM_INTEGER_SIXTEEN);
                String[] split1 = param.split(HttpToolConstant.CHAR_EQUAL);
                map.put(split1[0], split1[1]);
                setParam(map);
            }
        }

        return this;
    }

    /**
     * 设置参数。
     *
     * @param param 入参
     * @return HttpTool实例
     */
    public HttpTool setParam(Map param) {
        this.paramMap = MapUtils.toMapStrObj(param);
        return this;
    }

    /**
     * 设置入参, 同时设置附件参数的公共名称。
     * <br> 附件参数的公共名称适用于接收File对象数组或集合的接口，需要绑定唯一名称。
     *
     * @param param 入参
     * @param sameName 附件参数的公共名称
     * @return HttpTool实例
     */
    public HttpTool setParam(Map param, String sameName) {
        // 设置同名
        this.sameName = sameName;
        this.setParam(param);
        return this;
    }

    /**
     * 设置表单传输分割标识。
     * @param BOUNDARY 表单传输分割标识
     */
    private void setBOUNDARY(String BOUNDARY) {
        this.BOUNDARY = BOUNDARY;
    }

    /**
     * 设置表单传输分割标识 BOUNDARY。
     * @return 表单传输分割标识（UUID）
     */
    public String getBOUNDARY() {
        return BOUNDARY;
    }

    /**
     * 设置请求方式，默认GET。
     * <br>尽量用 {@linkplain HttpToolConstant HttpToolConstant} 常量类中 以 "REQUEST_METHOD" 开头的常量，以防止手写错误。
     *
     * @param method GET、POST……
     * @return HttpTool实例
     */
    public HttpTool setRequestMethod(String method) {
        this.requestMethod = method.trim().toUpperCase();
        return this;
    }

    /**
     * 获得请求方式, 默认GET。
     *
     * @return 请求方式
     */
    public String getRequestMethod() {
        return requestMethod;
    }

    /**
     * 设置连接时间(连接主机超时)，单位秒，默认10秒。
     *
     * @param connectTimeout 连接时间(连接主机超时)，单位秒
     * @return HttpTool实例
     */
    public HttpTool setConnectTimeout(Integer connectTimeout) {
        this.connectTimeout = connectTimeout * 1000;
        return this;
    }

    /**
     * 获得连接时间(连接主机超时)，单位秒。
     *
     * @return 连接时间(连接主机超时)，单位秒
     */
    public Integer getConnectTimeout() {
        return connectTimeout;
    }

    /**
     * 设置读取时间(缓存最长时间)，单位秒，默认30秒。
     *
     * @param readTimeout 读取时间(缓存最长时间)，单位秒
     * @return HttpTool实例
     */
    public HttpTool setReadTimeout(Integer readTimeout) {
        this.readTimeout = readTimeout * 1000;
        return this;
    }

    /**
     * 设置读取时间(缓存最长时间)，单位秒。
     *
     * @return 读取时间(缓存最长时间)，单位秒
     */
    public Integer getReadTimeout() {
        return readTimeout;
    }

    /**
     * 获得URL。
     * @return URL
     */
    public String getUrl() {
        return url;
    }

    /**
     * 设置URL。
     * @param url URL
     * @return HttpTool实例
     */
    public HttpTool setUrl(String url) {
        this.url = url;
        return this;
    }

    /**
     * 获得参数，Map类型。
     *
     * @return 参数
     */
    public Map<String, Object> getParamMap() {
        return paramMap;
    }

    /**
     * 设置参数。
     *
     * @param paramMap 入参
     */
    private void setParamMap(Map<String, Object> paramMap) {
        this.paramMap = paramMap;
    }

    /**
     * 获得文件传输时候的公共名称。
     *
     * @return 文件传输时候的公共名称
     */
    public String getSameName() {
        return sameName;
    }

    /**
     * 设置文件传输的公共名称。
     *
     * @param sameName 文件传输时候的公共名称
     * @return HttpTool实例
     */
    public HttpTool setSameName(String sameName) {
        this.sameName = sameName;
        return this;
    }

    /**
     * 设置是否以字符流的方式读取返回结果。
     *
     * @param TXT true 字符流读取， false 字节流读取
     * @return HttpTool实例
     */
    public HttpTool setTXT(Boolean TXT) {
        isTXT = TXT;
        return this;
    }

    /**
     * 获得是否以字符流的方式读取返回结果。
     *
     * @return  true 字符流读取， false 字节流读取
     */
    public Boolean getTXT() {
        return isTXT;
    }

    /**
     * 获取请求头。
     *
     * @return 请求头
     */
    public Map<String, String> getHeader() {
        return header;
    }

    /**
     * 设置请求头。
     *
     * @param header 请求头Map
     * @return HttpTool实例
     */
    public HttpTool setHeader(Map<String,String> header) {
        for (Map.Entry<String,String> entry : header.entrySet()) {
            this.header.put(entry.getKey(), entry.getValue());
        }
        return this;
    }

    /**
     * 设置请求头。
     *
     * @param key 请求头 键
     * @param value 请求头 值
     * @return HttpTool实例
     */
    public HttpTool setHeader(String key, String value) {
        this.header.put(key.trim(), value.trim());
        return this;
    }

    /**
     * 获得浏览器版本，默认PostmanRuntime/7.28.4。
     *
     * @return 浏览器版本
     */
    public String getUserAgent() {
        return MapUtils.getString(this.header, HttpToolConstant.HTTP_STR_USER_AGENT, HttpToolConstant.STR_EMPTY);
    }

    /**
     * 设置浏览器版本，默认PostmanRuntime/7.28.4。
     * 可跨过一些校验
     *
     * @param userAgent 浏览器版本
     * @return HttpTool实例
     */
    public HttpTool setUserAgent(String userAgent) {
        this.header.put(HttpToolConstant.HTTP_STR_USER_AGENT, userAgent.trim());
        return this;
    }

    /**
     * 设置编码，默认UTF-8。
     * <br>尽量用 {@linkplain HttpToolConstant HttpToolConstant} 常量类中 以 "CODE_FORMAT" 开头的常量，以防止手写错误。
     *
     * @param charset 编码
     * @return HttpTool实例
     */
    public HttpTool setCharset(String charset) {
        this.header.put(HttpToolConstant.HTTP_STR_CHARSET,charset.trim().toUpperCase());
        return this;
    }

    /**
     * 获取编码，默认UTF-8.
     *
     * @return 编码
     */
    public String getCharset() {
        return MapUtils.getString(this.header, HttpToolConstant.HTTP_STR_CHARSET, HttpToolConstant.STR_EMPTY);
    }

    /**
     * 设置contentType：请求端向服务端的传输类型，默认application/json。
     * <br>尽量用 {@linkplain HttpToolConstant HttpToolConstant} 常量类中 以 "CONTENT_TYPE" 开头的常量，以防止手写错误。
     *
     * @param contentType 传输类型
     * @return HttpTool实例
     */
    public HttpTool setContentType(String contentType) {
        //设置contentType
        this.header.put(HttpToolConstant.HTTP_STR_CONTENT_TYPE,contentType.trim());
        return this;
    }

    /**
     * 获得contentType：请求端向服务端的传输类型，默认application/json。
     *
     * @return  传输类型
     */
    public String getContentType() {
        return MapUtils.getString(this.header, HttpToolConstant.HTTP_STR_CONTENT_TYPE, HttpToolConstant.STR_EMPTY);
    }

    /**
     * 获得接收请求类型。
     *
     * @return 收请求类型
     */
    public String getAcceptType() {
        return MapUtils.getString(this.header, HttpToolConstant.HTTP_STR_ACCEPT, HttpToolConstant.STR_EMPTY);
    }

    /**
     * 设置接收请求类型。
     * <br>尽量用 {@linkplain HttpToolConstant HttpToolConstant} 常量类中 以 "ACCEPT" 开头的常量，以防止手写错误。
     *
     * @param acceptType 收请求类型
     * @return HttpTool实例
     */
    public HttpTool setAcceptType(String acceptType) {
        this.header.put(HttpToolConstant.HTTP_STR_ACCEPT,acceptType.trim());
        return this;
    }

    /**
     * 设置请求头connection类型。
     *
     * @param open true:keep-alive,false:close
     * @return HttpTool实例
     */
    public HttpTool setConnect(Boolean open) {
        if (!open) {
            this.header.put(HttpToolConstant.HTTP_STR_Connect, HttpToolConstant.CONNECTION_CLOSE);
        } else {
            this.header.put(HttpToolConstant.HTTP_STR_Connect, HttpToolConstant.CONNECTION_OPEN);
        }
        return this;
    }

    /**
     * 设置请求头connection类型状态。
     *
     * @return connection类型状态,keep-alive或者close
     */
    public String getConnect() {
        return MapUtils.getString(this.header, HttpToolConstant.HTTP_STR_Connect, HttpToolConstant.STR_EMPTY);
    }

    /**
     * 获得代理地址。
     *
     * @return 代理地址
     */
    public String getProxyUrl() {
        return proxyUrl;
    }

    /**
     * 设置代理地址。
     *
     * @param proxyUrl 代理地址
     */
    public void setProxyUrl(String proxyUrl) {
        this.proxyUrl = proxyUrl;
        if (null == this.proxyPort) {
            this.proxyPort = 80;
        }
    }

    /**
     * 获得代理端口。
     *
     * @return 代理端口
     */
    public Integer getProxyPort() {
        return proxyPort;
    }

    /**
     * 设置代理端口。
     *
     * @param proxyPort  设置代理端口
     */
    public void setProxyPort(Integer proxyPort) {
        if (proxyPort <= 0 || proxyPort == null) {
            proxyPort = 80;
        }
        this.proxyPort = proxyPort;
    }

// --------------------------------------------------------------------------------------
    // -----------触发方法----------------------------------------------------------------
    // --------------------------------------------------------------------------------------

    /**
     * get请求静态方法，入参可拼接在url的后面。
     *
     * @param url 请求地址
     * @throws Exception 请求异常
     * @return Http工具对象，通过&nbsp;{@linkplain HttpTool#getResult() getResult}&nbsp;方法获取请求结果，通过
     * &nbsp;{@linkplain HttpTool#getCode() getCode}&nbsp;方法获取请求状态码
     */
    public static HttpTool sendGet(String url) throws Exception{
        HttpTool tool = getInstance(url);
        send(tool);
        return tool;
    }

    /**
     * get请求静态方法。
     *
     * @param url 请求地址
     * @param param 入参
     * @throws Exception 请求异常
     * @return Http工具对象，通过&nbsp;{@linkplain HttpTool#getResult() getResult}&nbsp;方法获取请求结果，通过
     * &nbsp;{@linkplain HttpTool#getCode() getCode}&nbsp;方法获取请求状态码
     */
    public static HttpTool sendGet(String url, Map param) throws Exception{
        HttpTool tool = getInstance(url);
        tool.setParam(param);
        send(tool);
        return tool;
    }

    /**
     * post请求静态方法。
     *
     * @param url 请求地址
     * @param param 入参
     * @param isJson 是否是json传输，true 是json传输，false是表单传输
     * @throws Exception 请求异常
     * @return Http工具对象，通过&nbsp;{@linkplain HttpTool#getResult() getResult}&nbsp;方法获取请求结果，通过
     * &nbsp;{@linkplain HttpTool#getCode() getCode}&nbsp;方法获取请求状态码
     */
    public static HttpTool sendPost(String url, Map param, boolean isJson) throws Exception {
        HttpTool tool = getInstance(url);
        tool.setParam(param);
        tool.setRequestMethod(HttpToolConstant.REQUEST_METHOD_POST);
        // 如果不是json传输，设置为表单传输
        if (isJson) {
            tool.setContentType(HttpToolConstant.CONTENT_TYPE_APPLICATION_JSON);
        }
        send(tool);
        return tool;
    }


    /**
     * 通用发送Http请求。
     * <br>通过&nbsp;{@linkplain HttpTool#getResult() getResult}&nbsp;方法获取请求结果，通过
     * &nbsp;{@linkplain HttpTool#getCode() getCode}&nbsp;方法获取请求状态码
     *
     * @return 请求结果
     * @throws Exception 请求异常
     */
    public String send() throws Exception {
        return send(this);
    }

    /**
     * 封装内部方法
     * @param tool 工具类
     * @throws Exception 请求异常
     * @return 结果
     */
    private static String send(HttpTool tool) throws Exception {
        HttpService httpService = HttpService.getInstance();

        String res = "";
        HttpURLConnection httpURLConnection = null;

        try {
            // 校验入参
            httpService.check(tool);

            // 创建链接对象
            httpURLConnection = httpService.build(tool);

            // 设置请求类型
            httpURLConnection.setRequestMethod(tool.getRequestMethod());

            // 如果是post请求
            if (HttpToolConstant.REQUEST_METHOD_POST.equals(tool.getRequestMethod())) {
                // 允许写出
                httpURLConnection.setDoOutput(true);
                // 允许读入
                httpURLConnection.setDoInput(true);
                // 不使用缓存
                httpURLConnection.setUseCaches(false);
            }

            if (tool.getConnectTimeout() != 0 && tool.getReadTimeout() != 0) {
                // 设置连接时间(连接主机超时)
                httpURLConnection.setConnectTimeout(tool.getConnectTimeout());
                // 设置读取时间(缓存最长时间)
                httpURLConnection.setReadTimeout(tool.getReadTimeout());
            }

            // 设置请求头, 表单传输特殊处理
            if (tool.getContentType().contains(HttpToolConstant.CONTENT_TYPE_FORM_DATA)) {
                tool.getHeader().put(HttpToolConstant.HTTP_STR_CONTENT_TYPE, tool.getContentType() + "; boundary=" + tool.getBOUNDARY());
            }
            httpService.setHeader(httpURLConnection, tool.getHeader());

            // 存在入参且不是GET请求
            boolean docon = (tool.getParamMap() != null || tool.getParam() != null) && !HttpToolConstant.REQUEST_METHOD_GET.equals(tool.getRequestMethod());
            if (docon) {
                // 输出流对象
                DataOutputStream ds = null;
                try {
                    ds = new DataOutputStream(httpURLConnection.getOutputStream());
                } catch (UnknownHostException e) {
                    throw new UnknownHostException("无法解析地址：" + tool.getUrl() + ";可能是地址无法ping通，域名无法解析，ip端口错误等");
                }
                // ----设置入参----
                // 如果是multipart/form-data类型传输
                if (tool.getContentType().contains(HttpToolConstant.HTTP_STR_FORM_DATA)) {
                    httpService.setParamFormData(ds, tool.getParamMap(), tool.getBOUNDARY(), tool.getCharset(), tool.getSameName());
                } else if (tool.getContentType().contains(HttpToolConstant.STR_JSON)) {
                    // 如果是json传输
                    ds.write(JSON.toJSONString(tool.getParamMap()).getBytes(tool.getCharset()));
                } else if (tool.getContentType().contains(HttpToolConstant.CONTENT_TYPE_TEXT_PLAIN)) {
                    // 如果是text/plain传输
                    ds.write(tool.getParam().getBytes(tool.getCharset()));
                } else {
                    // 其他传输
                    ds.write(httpService.mapToStringParam(tool.getParamMap()).getBytes(tool.getCharset()));
                }
                // 输出流刷新关闭
                ds.flush();
                ds.close();
            } else {
                // 不存在入参 或 GET请求已将所有参数拼接在URL之后 直接连接
                httpURLConnection.connect();
            }

            // 请求结果处理
            // 获取请求响应状态码
            int responseCode = httpURLConnection.getResponseCode();
            tool.setCode(responseCode);
            tool.setMsg(httpService.msgBuild(responseCode,tool));

            // 如果请求状态码为200
            if (HttpToolConstant.SC_OK == responseCode) {
                res = httpService.handleResponse(httpURLConnection, tool.getTXT(), tool.getCharset());
                tool.setResult(res);
            }


        } catch (Exception e) {
            throw e;
        }  finally {
            if (null != httpURLConnection) {
                // 关闭连接
                httpURLConnection.disconnect();
            }
        }

        return res;
    }
}
